package ru.romannep.pc.chopperbattle;

import java.util.List;

/**
 * Грани игрового элемента, которыми он может сталкиваться
 *
 */
enum ContactEdge {

    CONTACT_TOP, CONTACT_RIGHT, CONTACT_BOTTOM, CONTACT_LEFT, CONTACT_NULL
}

/**
 * Движущийся игровой элемент. Имеет поля скоростей по осям и ускорений.
 *
 */
 public class MovingGameElement extends GameElement {

    public float speedX, speedY, accX, accY;

    public MovingGameElement(float x, float y, float sx, float sy, float elast, float speedX, float speedY, float accX, float accY) {
        super(x, y, sx, sy, elast);
        this.speedX = speedX;
        this.speedY = speedY;
        this.accX = accX;
        this.accY = accY;
    }

    public MovingGameElement(int type, float x, float y, float sx, float sy, float elast, float speedX, float speedY, float accX, float accY) {
        super(type, x, y, sx, sy, elast);
        this.speedX = speedX;
        this.speedY = speedY;
        this.accX = accX;
        this.accY = accY;
    }

    public MovingGameElement(float x, float y, float sx, float sy, float elast) {
        super(x, y, sx, sy, elast);
    }

    /**
     * Проверяет есть ли пересечение с переданным в параметре игровым элементом
     *
     * @param obj
     * @return true - если есть пересечение
     */
    public boolean intersects(GameElement obj) {
        return (x < obj.x + obj.sx) && (x + sx > obj.x) && (y < obj.y + obj.sy) && (y + sy > obj.y);
    }

    /**
     * Проверяет есть ли пересечение (хотябы одно) c переданным в параметре
     * списке игровых елементов
     *
     * @param borders
     * @return GameElement - первое попавшееся пересечение
     */
    public GameElement intersectedElement(List<GameElement> elements) {
        for (GameElement element : elements) {
            if (!element.equals(this)
                    && intersects(element)) {
                return element;
            }
        }
        return null;
    }

    private boolean isContactVertical(float x, float y) {
        if (x < 0) {
            return false;
        }
        if (y < 0) {
            return true;
        }
        return Math.abs(speedY / speedX) > y / x;
    }

    /**
     * Возвращает грань которым текущий элемент стукнулся о элемент, переданный
     * в параметре
     *
     * @param obj
     * @return
     */
    private ContactEdge getContactEdge(GameElement obj) {
        boolean dx = (speedX > 0);
        boolean dy = (speedY > 0);

        if (dx && dy) {
            if (isContactVertical(obj.x - x - sx + speedX, obj.y - y - sy + speedY)) {
                return ContactEdge.CONTACT_RIGHT;
            } else {
                return ContactEdge.CONTACT_BOTTOM;
            }
        } else if (dx && !dy) {
            if (isContactVertical(obj.x - x - sx + speedX, y - speedY - obj.y - obj.sy)) {
                return ContactEdge.CONTACT_RIGHT;
            } else {
                return ContactEdge.CONTACT_TOP;
            }
        } else if (!dx && dy) {
            if (isContactVertical(x - speedX - obj.x - obj.sx, obj.y - y - sy + speedY)) {
                return ContactEdge.CONTACT_LEFT;
            } else {
                return ContactEdge.CONTACT_BOTTOM;
            }
        } else if (!dx && !dy) {
            if (isContactVertical(x - speedX - obj.x - obj.sx, y - speedY - obj.y - obj.sy)) {
                return ContactEdge.CONTACT_LEFT;
            } else {
                return ContactEdge.CONTACT_TOP;
            }
        }
        return ContactEdge.CONTACT_NULL;
    }

    /**
     * Изменяет скорость исходя из ускорения
     */
    public void changeSpeed() {
        speedX += accX;
        speedY += accY;
    }

    /**
     * Выполняет сдвиг элемента так, чтобы он не пересекал element переданый в
     * качетве параметра Возвращает время, которое еще осталось пройти элементу
     *
     * @param contactEdge - край, которым элемент ударился о препятствие
     * @param stepTime - время, за которое он переместился и стал пересекать
     * препятствие
     * @param element - препятствие
     * @param delta - корректировка позиции
     * @return
     */
    private float moveBack(ContactEdge contactEdge, float stepTime, GameElement element, float delta) {
        float restTime = 0;
        switch (contactEdge) {
            case CONTACT_RIGHT:
                restTime += Math.abs((x + sx - element.x) / (speedX * stepTime));
                y = y - speedY / speedX * (x + sx - element.x);
                x = element.x - sx - delta;
                break;
            case CONTACT_LEFT:
                restTime += Math.abs((element.x + element.sx - x) / (speedX * stepTime));
                y = y + speedY / speedX * (element.x + element.sx - x);
                x = element.x + element.sx + delta;
                break;
            case CONTACT_TOP:
                restTime += Math.abs((element.y + element.sy - y) / (speedY * stepTime));
                x = x + speedX / speedY * (element.y + element.sy - y);
                y = element.y + element.sy + delta;
                break;
            case CONTACT_BOTTOM:
                restTime += Math.abs((y + sy - element.y) / (speedY * stepTime));
                x = x - speedX / speedY * (y + sy - element.y);
                y = element.y - sy - delta;
        }//switch

        return restTime;
    }

    /**
     * Выполняет перемещение элемента на единицу времени по игровому полю с
     * другими элементами заданными в параметре. В случае, когда при перемещении
     * элемент встречает препядствие, выполняется вызов метода incomingContact
     * элемента, в который попадает наш элемент. При необходимости выполняется
     * отражение скоростей (если метод onContact возвращает true)
     *
     * @param time - время
     * @param elements - другие игровые элементы
     */
    public void move(float time, List<GameElement> elements) {


        changeSpeed();
        float processedTime = 0;
        //чтобы объект не пролетал насквозь препядствия, установим, что в единицу времени он может перемещатся
        //менее чем на свой размер
        float tickTime = Math.min(Math.abs(speedX * time) > sx ? (sx) / (Math.abs(speedX * time) + 1) : 1, Math.abs(speedY * time) > sy ? sy / (Math.abs(speedY * time) + 1) : 1);
        float stepTime;
        float preX, preY;
        GameElement element;
        GameElement elementContact = intersectedElement(elements);
        float restTime;
        ContactEdge contactEdge;
        float delta;

        //если в начале движения наш элемент уже пересекает другой, перемещение не выполняем
        if (elementContact != null) {
            elementContact.incomingContact(this, ContactEdge.CONTACT_NULL);
            onContact(elementContact, ContactEdge.CONTACT_NULL);
            return;
        }

        while (processedTime < time) {
            stepTime = tickTime > (time - processedTime) ? (time - processedTime) : tickTime;

            preX = x;
            preY = y;

            x += speedX * stepTime;
            y += speedY * stepTime;

            restTime = 0f;

            element = intersectedElement(elements);
            elementContact = null;
            contactEdge = ContactEdge.CONTACT_NULL;

            //проверяем наличие столкновения с другими элементами
            //и возващаемся на позицию до соприкосновения
            while (element != null) {
                delta = 0;
                if (element.equals(elementContact)) {
                    //по идее такого быть не может, т.к. при контакте мы сдвигаем объект так, 
                    //чтобы он не пересекал препятствие, однако из-за неточности операций с числами float
                    //может быть что и после сдвига пересечение наблюдяется
                    delta = 1 / 1000f;
                }

                elementContact = element;
                contactEdge = getContactEdge(element);

                restTime += moveBack(contactEdge, stepTime, element, delta);

                //чтобы объект не "шумел" из-за float операций при неупругом касании, восстановим его позицию 
                if (restTime > 0.99 * stepTime) {
                    restTime = 1;
                    x = preX;
                    y = preY;
                }

                element = intersectedElement(elements);

            }

            if (elementContact != null) {
                //оповестим объект, что в него попали
                elementContact.incomingContact(this, contactEdge);
                if (onContact(elementContact, contactEdge)) {
                    //если нужно выполнить отражение - выполним
                    if (contactEdge == ContactEdge.CONTACT_LEFT || contactEdge == ContactEdge.CONTACT_RIGHT) {
                        speedX = Math.abs(speedX) > elementContact.elast ? -Math.signum(speedX) * (Math.abs(speedX) - elementContact.elast) : 0;
                    } else {
                        speedY = Math.abs(speedY) > elementContact.elast ? -Math.signum(speedY) * (Math.abs(speedY) - elementContact.elast) : 0;
                    }
                } else {
                    break;
                }
            }

            //чтобы избежать зацикливания, будем в любом случае увеличивать прошедшее время на 0.1 от единицы
            processedTime += (stepTime - restTime) > (tickTime / 10) ? (tickTime - restTime) : (tickTime / 10);
        }


    }

    /**
     * Вызывается при столкновении с другим объектом. Определяет - необходимо ли
     * выполнять отражение скоростей
     *
     * @param obj
     * @return true если после столкновения нужно выполнить отражение
     */
    protected boolean onContact(GameElement obj, ContactEdge contactEdge) {
        return true;
    }
}